package org.lexapp.ftp
{
	import com.carlcalderon.arthropod.Debug;
	
	import flash.events.Event;
	import flash.utils.ByteArray;
	
	import mx.managers.CursorManager;
	
	import pl.maliboo.ftp.core.FTPClient;
	import pl.maliboo.ftp.events.FTPEvent;
	
	/**
	 * This class has all the functionnalities of a standard FTP client : directory listing, batch upload[, batch download]
	 * It contains some of the code of the v1 Uploader, UploaderClient class.
	 * It uses pl.maliboo.ftp.core.FTPClient
	 * events: AUTH_ERROR, AUTH_SUCCESS, CONNECTION_SUCCESS, DIRECTORY_LISTING_ERROR, DIRECTORY_LISTING_SUCCESS
	 * upload events: FILE_UPLOAD_FINISHED, FILE_UPLOAD_STARTED, BATCH_UPLOAD_FINISHED, BATCH_UPLOAD_STARTED, UPLOAD_ERROR, UPLOAD_PROGRESS, 
	 * [to do: download events: FILE_DOWNLOAD_FINISHED, FILE_DOWNLOAD_STARTED, BATCH_DOWNLOAD_FINISHED, BATCH_DOWNLOAD_STARTED, DOWNLOAD_ERROR, DOWNLOAD_PROGRESS, ]
	 */
	public class RemoteFileBrowserApplication
	{
		private const className:String = "org.lexapp.ftp.RemoteFileBrowserApplication";
		public static var SOCKET_BUFFER_SIZE:uint = 1000000;
		//////////////////////////////////////////////////////////////
		// Group: Log level constants
		//////////////////////////////////////////////////////////////

		public static const DEBUG_MODE:Boolean=true;
		public static const LOG_LEVEL_DEBUG1:uint=0;
		public static const LOG_LEVEL_DEBUG2:uint=2;
		public static const LOG_LEVEL_DEBUG3:uint=4;
		public static const LOG_LEVEL_OK:uint=6;
		public static const LOG_LEVEL_KO:uint=8;
		public static const LOG_LEVEL_NETWORK:uint=10;
		public static const LOG_LEVEL_UI:uint=12;
		public static const LOG_LEVEL_APPLICATION:uint=13;
		public static const LOG_LEVEL_WARNING:uint=14;
		public static const LOG_LEVEL_ERROR:uint=16;

		//////////////////////////////////////////////////////////////
		// Group: Events
		//////////////////////////////////////////////////////////////

		public static const EVENT_CONNECTION_SUCCESS:String = "CONNECTION_SUCCESS";
		public static const EVENT_CONNECTION_ERROR:String = "CONNECTION_ERROR";
//		public static const EVENT_AUTH_ERROR:String = "AUTH_ERROR";
		public static const EVENT_AUTH_SUCCESS:String = "AUTH_SUCCESS";
//		public static const EVENT_DIRECTORY_LISTING_ERROR:String = "DIRECTORY_LISTING_ERROR";
		public static const EVENT_DIRECTORY_LISTING_SUCCESS:String = "DIRECTORY_LISTING_SUCCESS";
		public static const EVENT_CURRENT_DIRECTORY_CHANGED:String = "EVENT_CURRENT_DIRECTORY_CHANGED";
		
		public static const EVENT_FILE_UPLOAD_FINISHED:String = "FILE_UPLOAD_FINISHED";
		public static const EVENT_FILE_UPLOAD_STARTED:String = "FILE_UPLOAD_STARTED";
		public static const EVENT_BATCH_UPLOAD_FINISHED:String = "BATCH_UPLOAD_FINISHED";
		public static const EVENT_BATCH_UPLOAD_STARTED:String = "BATCH_UPLOAD_STARTED";
		public static const EVENT_UPLOAD_ERROR:String = "UPLOAD_ERROR";
		public static const EVENT_UPLOAD_PROGRESS:String = "UPLOAD_PROGRESS";

		public static const ERROR_BROWSE_CLIENT_BUSY:String = "BROWSE_CLIENT_BUSY";
		public static const ERROR_FTP_ERROR:String = "FTP_ERROR";
		public static const ERROR_FILE_ALLREADY_UPLOADING:String = "ERROR_FILE_ALLREADY_UPLOADING";
		
		
		//////////////////////////////////////////////////////////////
		// Group: attributes
		//////////////////////////////////////////////////////////////
		
		/**
		 * FTPClient object used for browsing the server
		 */
		protected var browseProcess:FTPClient;
		/**
		 * array of UploaderClient objects
		 */
		public static var uploadProcessArray:Array;
		/**
		 * array of UploaderClient objects
		 * not implemented yet
		 * AIR version only?
		 */
		// protected var downloadProcessArray:Array;
		/**
		 * current remote directory path
		 */
		public var currentRemotePath:String;
		[Bindable]
		/**
		 * list of files of the current remote directory
		 */
		public var remoteFilesArray:Array;
		/**
		 * user login
		 */
		public var userLogin:String;
		/**
		 * user password
		 */
		public var userPass:String;
		/**
		 * ftp host
		 */
		public var ftpHost:String;
		/**
		 * ftp port
		 */
		public var ftpPort:uint;
		/**
		 * indicates wether the browse process is connected or not
		 */
		public var connected:Boolean = false;
		/**
		 * indicates wether a command process is pending or not
		 */
		private var _processing:Boolean = false;
		public function get processing():Boolean
		{
			return _processing;
		}
		public function set processing(val:Boolean):void
		{
			_processing=val;
			if(val)
				CursorManager.setBusyCursor();
			else
				CursorManager.removeBusyCursor();
		}
		
		//////////////////////////////////////////////////////////////
		// Group: init methods
		//////////////////////////////////////////////////////////////

		/**
		 * constructor
		 */
		public function RemoteFileBrowserApplication()
		{
			initDebug();
		}
		/**
		 * initialize all variables and arrays
		 */
		public function initialize():void
		{
			trace("initialize",className);
			
			//super.initialize();
			
			//
			currentRemotePath = "";
			remoteFilesArray = new Array;
			
			if (!uploadProcessArray)
				uploadProcessArray = new Array;
			
			// init ftp client object
			browseProcess = new FTPClient;
			addBrowseEventListeners(browseProcess);
		}
		/**
		 * add events to the browse ftpClient
		 */
		public function addBrowseEventListeners(ftpClient:FTPClient):void
		{
			ftpClient.addEventListener(FTPEvent.CONNECTED,connectedHandler);
			ftpClient.addEventListener(FTPEvent.DISCONNECTED,disconnectedHandler);
			ftpClient.addEventListener(FTPEvent.LOGGED,authSuccessHandler);
//			ftpClient.addEventListener(FTPEvent.COMMAND,commandHandler);
			ftpClient.addEventListener(FTPEvent.CHANGE_DIR,pwdHandler);
//			ftpClient.addEventListener(FTPEvent.CREATE_DIR,createDirHandler);
//			ftpClient.addEventListener(FTPEvent.DELETE_DIR,deleteDirHandler);
//			ftpClient.addEventListener(FTPEvent.DELETE_FILE,deleteFileHandler);
//			ftpClient.addEventListener(FTPEvent.DOWNLOAD,downloadHandler);
			ftpClient.addEventListener(FTPEvent.LISTING,listingHandler);
//			ftpClient.addEventListener(FTPEvent.PROGRESS,progressHandler);
//			ftpClient.addEventListener(FTPEvent.RENAME_FILE,renameFileHandler);
//			ftpClient.addEventListener(FTPEvent.RESPONSE,responseHandler);
//			ftpClient.addEventListener(FTPEvent.UPLOAD,uploadHandler);
//			ftpClient.addEventListener(FTPEvent.UPLOAD_PROGRESS,uploadProgressHandler);
			ftpClient.addEventListener(FTPEvent.INVOKE_ERROR,invokeErrorHandler);
			ftpClient.addEventListener(FTPEvent.PWD,pwdHandler);
//			ftpClient.addEventListener(FTPEvent.ABORT,abortHandler);
		}
		/**
		 * remove events from the browse ftpClient
		 */
		public function removeBrowseEventListeners(ftpClient:FTPClient):void
		{
			ftpClient.removeEventListener(FTPEvent.CONNECTED,connectedHandler);
			ftpClient.removeEventListener(FTPEvent.DISCONNECTED,disconnectedHandler);
			ftpClient.removeEventListener(FTPEvent.LOGGED,authSuccessHandler);
//			ftpClient.removeEventListener(FTPEvent.COMMAND,commandHandler);
			ftpClient.removeEventListener(FTPEvent.CHANGE_DIR,pwdHandler);
//			ftpClient.removeEventListener(FTPEvent.CREATE_DIR,createDirHandler);
//			ftpClient.removeEventListener(FTPEvent.DELETE_DIR,deleteDirHandler);
//			ftpClient.removeEventListener(FTPEvent.DELETE_FILE,deleteFileHandler);
//			ftpClient.removeEventListener(FTPEvent.DOWNLOAD,downloadHandler);
			ftpClient.removeEventListener(FTPEvent.LISTING,listingHandler);
//			ftpClient.removeEventListener(FTPEvent.PROGRESS,progressHandler);
//			ftpClient.removeEventListener(FTPEvent.RENAME_FILE,renameFileHandler);
//			ftpClient.removeEventListener(FTPEvent.RESPONSE,responseHandler);
//			ftpClient.removeEventListener(FTPEvent.UPLOAD,uploadHandler);
//			ftpClient.removeEventListener(FTPEvent.UPLOAD_PROGRESS,uploadProgressHandler);
			ftpClient.removeEventListener(FTPEvent.INVOKE_ERROR,invokeErrorHandler);
			ftpClient.removeEventListener(FTPEvent.PWD,pwdHandler);
//			ftpClient.removeEventListener(FTPEvent.ABORT,abortHandler);
		}
		/**
		 * Store password, login, port and url.
		 */
		public function connect(login:String, password:String, host:String, port:uint=21,proxyHost:String="",proxyPort:uint=21):void
		{				
			log("connect "+login+"@"+host+":"+port,className,LOG_LEVEL_NETWORK);
			if (proxyHost=="")
			{
				// store connection info
				userLogin = login;
				userPass = password;
				ftpPort = port;
				ftpHost = host;
			}
			else
			{
				// store connection info
				userLogin = login+"@"+host+":"+port;
				userPass = password;
				ftpPort = proxyPort;
				ftpHost = proxyHost;
			}
			
			if (!connected) {
				processing = true;
				browseProcess.connect(ftpHost,ftpPort);
			} else {
				dispatchEvent(new Event(EVENT_CONNECTION_SUCCESS));
			}
		}
		/**
		 * close connection
		 * cleanup
		 */
		public function closeConnection():void
		{
			log("closeConnection ",className);
			
			browseProcess.close();
			disconnectedHandler(null);
			//removeBrowseEventListeners(browseProcess);
			//initialize();
		}
	
		//////////////////////////////////////////////////////////////
		// Group: ftp commands
		//////////////////////////////////////////////////////////////

		/**
		 * change curent directory and call refresh
		 */
		public function cd(newPath:String):void
		{
			log("cd "+newPath,className);
			if (!processing) 
			{
				processing = true;
				browseProcess.cwd(newPath);
			}
			else
			{
				// to do: handle error
				
				// throw an error
				throw (new Error(ERROR_BROWSE_CLIENT_BUSY));
			}
		}
		/**
		 * returns the curent remote path
		 */
		public function pwd():void
		{
			log("pwd ", className);
			if (!processing) 
			{
				processing = true;
				browseProcess.pwd();
			}
			else
			{
				// to do: handle error
				
				// throw an error
				throw (new Error(ERROR_BROWSE_CLIENT_BUSY));
			}
		}
		/**
		 * call list() to update remoteFilesArray
		 */
		public function refresh():void
		{
			log("refresh",className);
			list(currentRemotePath);
		}
		/**
		 * list a directory
		 */
		public function list(path:String):void
		{
			log("list "+path,className);
			if (!processing) 
			{
				processing = true;
				browseProcess.list(path);
			}
			else
			{
				// to do: handle error
				
				// throw an error
				throw (new Error(ERROR_BROWSE_CLIENT_BUSY));
			}
		}
		/**
		 * download a file / add a process to downloadProcessArray
		 * not implemented yet
		 */
		/*
		public function downloadFile(filePathRemote:String,filePathLocal:String):void
		{
			browseProcess.getFile(filePathRemote,filePathLocal);
		}
		/**/
		/**
		 * upload a file / add a process to uploadHtpProcessArray
		 */
		public function uploadFile(srcFilePath:String,destFolder:String,destName:String,fileSize:uint,allowDuplicate:Boolean=false):UploaderClient
		{
			log("uploadFile "+srcFilePath+" -> "+destName+" IN "+destFolder,className,LOG_LEVEL_NETWORK);
			if (!processing) 
			{
				var existingUploadClient:UploaderClient = getUploadProcess(destFolder+destName);
				if(allowDuplicate || !existingUploadClient || existingUploadClient.state == UploaderClient.STATE_VIDEO_CANCEL)
				{
					return addUploadProcess(srcFilePath,destFolder,destName,fileSize);
				}
				else
				{
					// to do: handle error
					
					// throw an error
					throw (new Error(ERROR_FILE_ALLREADY_UPLOADING));
				}
			}
			else
			{
				// to do: handle error
				
				// throw an error
				throw (new Error(ERROR_BROWSE_CLIENT_BUSY));
			}
			return null;
		}
		/**
		 * use the REST ftp command to continue the upload of a file
		 */
		public function restoreFile(destFilePath:String):void
		{
			log("restoreFile "+destFilePath,className);
			if (!processing) 
			{
				processing = true;
			}
			else
			{
				// to do: handle error
				
				// throw an error
				throw (new Error(ERROR_BROWSE_CLIENT_BUSY));
			}
		}
		/**
		 * delete a file on the server
		 */
		public function deleteFile(filePath:String):void
		{
			log("deleteFile",className);
			if (!processing) 
			{
				processing = true;
			}
			else
			{
				// to do: handle error
				
				// throw an error
				throw (new Error(ERROR_BROWSE_CLIENT_BUSY));
			}
		}
		////////////////////////////////////////////////////////
		// Group: processes manipulation methods 
		////////////////////////////////////////////////////////
		/**
		 * find a process in the upload process array
		 */
		public static function getUploadProcess(destPath:String):UploaderClient
		{
			var idx:int = getUploadProcessIndex(destPath);
			if(idx>=0)
				return uploadProcessArray[idx];
			return null;
		}
		/**
		 * find a process index in the upload process array
		 */
		public static function getUploadProcessIndex(destPath:String):int
		{
			var idx:uint=0;
			while (idx < uploadProcessArray.length)
			{
				if ((uploadProcessArray[idx].destFolder+uploadProcessArray[idx].destName) == destPath)
				{
					log("getUploadProcessIndex "+destPath+" returns "+idx,"RemoteFileBrowserApplication",LOG_LEVEL_DEBUG1);
					return idx;
				}
				idx++;
			}
				log("getUploadProcessIndex "+destPath+" returns -1","RemoteFileBrowserApplication",LOG_LEVEL_KO);
			// process is not in the array
			return -1;
		}
		/**
		 * adds a process in the upload process array
		 */
		public function addUploadProcess(srcPath:String,destFolder:String,destName:String,fileSize:uint):UploaderClient
		{
			log("addUploadProcess "+srcPath+" -> "+destFolder+destName,className,LOG_LEVEL_APPLICATION);
			
			// init process
			var uploadProcess:UploaderClient = new UploaderClient(ftpHost,ftpPort);
			uploadProcess.addEventListener(FTPEvent.INVOKE_ERROR,handleUploadInvokeError);
//			uploadProcess.addEventListener(FTPEvent.CONNECTED, handleUploadConnect);
//			uploadProcess.addEventListener(FTPEvent.LOGGED, handleUploadLoginSuccess);
			uploadProcess.addEventListener(FTPEvent.DISCONNECTED, handleUploadDisconnect);
			uploadProcess.addEventListener(FTPEvent.UPLOAD_PROGRESS, handleUploadProgress);
			uploadProcess.addEventListener(FTPEvent.UPLOAD_REAL_END, handleUploadFinished);
//			uploadProcess.addEventListener(FTPEvent.LISTING, checkUploadFinished);

			// start upload
			uploadProcess.loginAndUpload(userLogin,userPass,srcPath,destFolder,destName,fileSize);
			
			// add the process to the list
			uploadProcessArray.push(uploadProcess);
			
			// 
			dispatchEvent(new Event(EVENT_FILE_UPLOAD_STARTED));
			if (uploadProcessArray.length==1)
			{
				dispatchEvent(new Event(EVENT_BATCH_UPLOAD_STARTED));
			}
			
			// return the new ftp client
			return uploadProcess;
		}
		/**
		 * remove a process from the upload process array
		 */
		public function removeUploadProcess(uploadProcess:UploaderClient):void
		{
			log("removeUploadProcess ",className,LOG_LEVEL_APPLICATION);

			// remove the process
			var idx:uint=getUploadProcessIndex(uploadProcess.destFolder+uploadProcess.destName);
			if (idx)
			{
				uploadProcessArray.splice(idx,1);
			}
			else
			{
				log("removeUploadProcess UPLOAD NOT IN THE LIST "+uploadProcess.destFolder+" - "+uploadProcess.destName,className,LOG_LEVEL_ERROR);
			}
			
			// init process
			uploadProcess.close();
			uploadProcess.removeEventListener(FTPEvent.INVOKE_ERROR,handleUploadInvokeError);
//			uploadProcess.removeEventListener(FTPEvent.CONNECTED, handleUploadConnect);
//			uploadProcess.removeEventListener(FTPEvent.LOGGED, handleUploadLoginSuccess);
			uploadProcess.removeEventListener(FTPEvent.DISCONNECTED, handleUploadDisconnect);
			uploadProcess.removeEventListener(FTPEvent.UPLOAD_PROGRESS, handleUploadProgress);
			uploadProcess.removeEventListener(FTPEvent.UPLOAD_REAL_END, handleUploadFinished);
//			uploadProcess.removeEventListener(FTPEvent.LISTING, checkUploadFinished);

			// delete
			uploadProcess = null;
			
			// check if all uploads are done
			if (uploadProcessArray.length<=0)
			{
				dispatchEvent(new Event(EVENT_BATCH_UPLOAD_FINISHED));
			}
		}
		//////////////////////////////////////////////////////////////
		// Group: browse process handlers
		//////////////////////////////////////////////////////////////
		/**
		 * current directory has changed 
		 */
		protected function pwdHandler(event:FTPEvent):void
		{
			log("pwdHandler " + event, className);
			processing = false;
			currentRemotePath = event.directory;
			dispatchEvent(new Event(EVENT_CURRENT_DIRECTORY_CHANGED));
		}

		/**
		 * handler for ftp errors
		 */
		protected function invokeErrorHandler(event:FTPEvent):void
		{
			log("invokeErrorHandler "+event,className,LOG_LEVEL_ERROR);
			
			// to do : handle error
			
			// throw an error
			throw (new Error(ERROR_FTP_ERROR));
		}
		/**
		 * connection with the server succeded
		 * => login
		 */
		protected function connectedHandler(event:FTPEvent):void
		{
			log("connectedHandler success => login as "+userLogin,className,LOG_LEVEL_OK);
			
			dispatchEvent(new Event(EVENT_CONNECTION_SUCCESS));
			
			browseProcess.login(userLogin,userPass);
		}
		/**
		 * authentication succeded
		 */
		protected function authSuccessHandler(event:FTPEvent=null):void
		{
			log("authSuccessHandler",className,LOG_LEVEL_OK);
			
			processing = false;
			connected = true;
			
			dispatchEvent(new Event(EVENT_AUTH_SUCCESS));
		}
		/**
		 * client disconected
		 */
		protected function disconnectedHandler(event:FTPEvent):void
		{
			log("disconnectedHandler",className,LOG_LEVEL_KO);
			
			processing = false;
			connected = false;
			
			//initialize();
			
			dispatchEvent(new Event(EVENT_CONNECTION_ERROR));
		}
		/**
		 * listing of a directory succeded
		 */
		protected function listingHandler(event:FTPEvent):void
		{
			processing = false;
			
			remoteFilesArray = event.listing;
			log("listingHandler - "+remoteFilesArray.length+" files",className,LOG_LEVEL_OK);
			
			dispatchEvent(new Event(EVENT_DIRECTORY_LISTING_SUCCESS));
		}
		//////////////////////////////////////////////////////////////
		// Group: upload process handlers
		//////////////////////////////////////////////////////////////
		/**
		 * upload process connected
		 * => login
		 */
/*		protected function handleUploadConnect(event:FTPEvent):void
		{
			log("handleUploadConnect "+event.target,className,LOG_LEVEL_NETWORK);
			(event.target as FTPClient).login(userLogin,userPass);
		}
*/
		/**
		 * upload process login success
		 * => login
		 */
/*		protected function handleUploadLoginSuccess(event:FTPEvent):void
		{
			log("handleUploadLoginSuccess "+event,className,LOG_LEVEL_NETWORK);
			
		}
*/
		/**
		 * upload progress handler
		 */
		protected function handleUploadProgress(event:FTPEvent):void
		{
			log("handleUploadProgress "+event,className,LOG_LEVEL_NETWORK);
			// dispatch the event
			dispatchEvent(new Event(EVENT_UPLOAD_PROGRESS));
		}
		/**
		 * upload end handler
		 * => remove the process
		 */
		protected function handleUploadInvokeError(event:FTPEvent):void
		{
			log("handleUploadInvokeError "+event.target,className,LOG_LEVEL_NETWORK);
			
			// clean up
			removeUploadProcess(event.target as UploaderClient);
			
			// dispatch the event
			dispatchEvent(new Event(EVENT_UPLOAD_ERROR));
			
			// to do: handle error
			
			// throw an error
			//??throw (new Error(EVENT_UPLOAD_ERROR));
		}
		/**
		 * upload error handler
		 * => remove the process
		 * => throw an error??
		 */
		protected function handleUploadDisconnect(event:FTPEvent):void
		{
			log("handleUploadDisconnect "+event.target,className,LOG_LEVEL_NETWORK);
			removeUploadProcess(event.target as UploaderClient);
			
			// to do: handle error
			
			// throw an error
			throw (new Error(ERROR_FTP_ERROR));
		}
		/**
		 * upload has finished successfully
		 * => remove the process
		 */
		protected function handleUploadFinished(event:FTPEvent):void
		{
			log("handleUploadFinished "+event.target,className,LOG_LEVEL_NETWORK);
			
			removeUploadProcess(event.target as UploaderClient);
			
			dispatchEvent(new Event(EVENT_FILE_UPLOAD_FINISHED));
		}

		////////////////////////////////////////////////////////
		// Group: utils
		////////////////////////////////////////////////////////
		/**
		 * adds a trailing slash to the given string if it does not have one allready
		 */
		public static function addTrailingSlash(str:String):String
		{
			if (str && str.charAt(str.length - 1) != '/') {
				str += '/';
			}
			return str;
		}
		/**
		 * replace backslashes with slashes in the given string
		 */
		public static function slashReplaceBackSlash(str:String):String
		{
			return str.replace(/[\/]+/g, '/');;
		}
		/**
		 * clear the logs
		 */
		public static function initDebug():void
		{
			Debug.clear();
		}
		/**
		 * duplicate an object
		 */
		public static function clone(source:Object):Object
		{
		    var myBA:ByteArray = new ByteArray();
		    myBA.writeObject(source);
		    myBA.position = 0;
		    return(myBA.readObject());
		}
		/**
		 * log a message
		 * 5 -> 
		 */
		public static function log(str:String,className:String="",logLevel:uint=LOG_LEVEL_DEBUG1):void
		{
			if (!DEBUG_MODE)
				return;
			
			if (className!="")
				str = className + " - " + str;
			
			trace (str);
			
			switch(logLevel)
			{
				case LOG_LEVEL_DEBUG1:
					Debug.log(str,0xFFFFFF);
				break;
				case LOG_LEVEL_DEBUG2:
					Debug.log("  "+str,0x999999);
				break;
				case LOG_LEVEL_DEBUG3:
					Debug.log("    "+str,0x333333);
				break;
				case LOG_LEVEL_OK:
					Debug.log(str,Debug.GREEN);
				break;
				case LOG_LEVEL_KO:
					Debug.log(str,Debug.RED);
				break;
				case LOG_LEVEL_NETWORK:
					Debug.log(str,Debug.BLUE);
				break;
				case LOG_LEVEL_UI:
					Debug.log(str,Debug.PINK);
				break;
				case LOG_LEVEL_APPLICATION:
					Debug.log(str,Debug.PINK);
				break;
				case LOG_LEVEL_WARNING:
					Debug.warning(str);
				break;
				case LOG_LEVEL_ERROR:
					Debug.error(str);
				break;
			}
		}
	}
}